diff --git a/swh/web/assets/src/bundles/browse/origin-save.js b/swh/web/assets/src/bundles/browse/origin-save.js index bf6e82247..4df34a04d 100644 --- a/swh/web/assets/src/bundles/browse/origin-save.js +++ b/swh/web/assets/src/bundles/browse/origin-save.js @@ -1,205 +1,252 @@ /** * Copyright (C) 2018 The Software Heritage developers * See the AUTHORS file at the top-level directory of this distribution * License: GNU Affero General Public License version 3, or any later version * See top-level LICENSE file for more information */ import {handleFetchError, csrfPost, isGitRepoUrl, removeUrlFragment} from 'utils/functions'; import {validate} from 'validate.js'; let saveRequestsTable; +function originSaveRequest(originType, originUrl, + acceptedCallback, pendingCallback, errorCallback) { + let addSaveOriginRequestUrl = Urls.browse_origin_save_request(originType, originUrl); + let grecaptchaData = {'g-recaptcha-response': grecaptcha.getResponse()}; + let headers = { + 'Accept': 'application/json', + 'Content-Type': 'application/json' + }; + let body = JSON.stringify(grecaptchaData); + csrfPost(addSaveOriginRequestUrl, headers, body) + .then(handleFetchError) + .then(response => response.json()) + .then(data => { + if (data.save_request_status === 'accepted') { + acceptedCallback(); + } else { + pendingCallback(); + } + grecaptcha.reset(); + }) + .catch(response => { + if (response.status === 403) { + errorCallback(); + } + grecaptcha.reset(); + }); +} + export function initOriginSave() { $(document).ready(() => { $.fn.dataTable.ext.errMode = 'throw'; fetch(Urls.browse_origin_save_types_list()) .then(response => response.json()) .then(data => { for (let originType of data) { $('#swh-input-origin-type').append(``); } }); saveRequestsTable = $('#swh-origin-save-requests').DataTable({ serverSide: true, ajax: Urls.browse_origin_save_requests_list('all'), columns: [ { data: 'save_request_date', name: 'request_date', render: (data, type, row) => { if (type === 'display') { let date = new Date(data); return date.toLocaleString(); } return data; } }, { data: 'origin_type', name: 'origin_type' }, { data: 'origin_url', name: 'origin_url', render: (data, type, row) => { if (type === 'display') { return `${data}`; } return data; } }, { data: 'save_request_status', name: 'status' }, { data: 'save_task_status', name: 'loading_task_status', render: (data, type, row) => { if (data === 'succeed') { let browseOriginUrl = Urls.browse_origin(row.origin_url); if (row.visit_date) { browseOriginUrl += `visit/${row.visit_date}/`; } return `${data}`; } return data; } } ], scrollY: '50vh', scrollCollapse: true, order: [[0, 'desc']] }); $('#swh-origin-save-requests-list-tab').on('shown.bs.tab', () => { saveRequestsTable.draw(); window.location.hash = '#requests'; }); $('#swh-origin-save-request-create-tab').on('shown.bs.tab', () => { removeUrlFragment(); }); let saveRequestAcceptedAlert = ``; let saveRequestPendingAlert = ``; let saveRequestRejectedAlert = ``; $('#swh-save-origin-form').submit(event => { event.preventDefault(); event.stopPropagation(); $('.alert').alert('close'); if (event.target.checkValidity()) { $(event.target).removeClass('was-validated'); let originType = $('#swh-input-origin-type').val(); let originUrl = $('#swh-input-origin-url').val(); - let addSaveOriginRequestUrl = Urls.browse_origin_save_request(originType, originUrl); - let grecaptchaData = {'g-recaptcha-response': grecaptcha.getResponse()}; - let headers = { - 'Accept': 'application/json', - 'Content-Type': 'application/json' - }; - let body = JSON.stringify(grecaptchaData); - csrfPost(addSaveOriginRequestUrl, headers, body) - .then(handleFetchError) - .then(response => response.json()) - .then(data => { - if (data.save_request_status === 'accepted') { - $('#swh-origin-save-request-status').html(saveRequestAcceptedAlert); - } else { - $('#swh-origin-save-request-status').html(saveRequestPendingAlert); - } - grecaptcha.reset(); - }) - .catch(response => { - if (response.status === 403) { - $('#swh-origin-save-request-status').css('color', 'red'); - $('#swh-origin-save-request-status').html(saveRequestRejectedAlert); - } - grecaptcha.reset(); - }); + + originSaveRequest(originType, originUrl, + () => $('#swh-origin-save-request-status').html(saveRequestAcceptedAlert), + () => $('#swh-origin-save-request-status').html(saveRequestPendingAlert), + () => { + $('#swh-origin-save-request-status').css('color', 'red'); + $('#swh-origin-save-request-status').html(saveRequestRejectedAlert); + }); } else { $(event.target).addClass('was-validated'); } }); $('#swh-show-origin-save-requests-list').on('click', (event) => { event.preventDefault(); $('.nav-tabs a[href="#swh-origin-save-requests-list"]').tab('show'); }); $('#swh-input-origin-url').on('input', function(event) { let originUrl = $(this).val().trim(); $(this).val(originUrl); $('#swh-input-origin-type option').each(function() { let val = $(this).val(); if (val && originUrl.includes(val)) { $(this).prop('selected', true); } }); }); if (window.location.hash === '#requests') { $('.nav-tabs a[href="#swh-origin-save-requests-list"]').tab('show'); } }); } export function validateSaveOriginUrl(input) { let validUrl = validate({website: input.value}, { website: { url: { schemes: ['http', 'https', 'svn', 'git'] } } }) === undefined; let originType = $('#swh-input-origin-type').val(); if (originType === 'git' && validUrl) { // additional checks for well known code hosting providers let githubIdx = input.value.indexOf('://github.com'); let gitlabIdx = input.value.indexOf('://gitlab.'); let gitSfIdx = input.value.indexOf('://git.code.sf.net'); let bitbucketIdx = input.value.indexOf('://bitbucket.org'); if (githubIdx !== -1 && githubIdx <= 5) { validUrl = isGitRepoUrl(input.value, 'github.com'); } else if (gitlabIdx !== -1 && gitlabIdx <= 5) { let startIdx = gitlabIdx + 3; let idx = input.value.indexOf('/', startIdx); if (idx !== -1) { let gitlabDomain = input.value.substr(startIdx, idx - startIdx); // GitLab repo url needs to be suffixed by '.git' in order to be successfully loaded validUrl = isGitRepoUrl(input.value, gitlabDomain) && input.value.endsWith('.git'); } else { validUrl = false; } } else if (gitSfIdx !== -1 && gitSfIdx <= 5) { validUrl = isGitRepoUrl(input.value, 'git.code.sf.net/p'); } else if (bitbucketIdx !== -1 && bitbucketIdx <= 5) { validUrl = isGitRepoUrl(input.value, 'bitbucket.org'); } } if (validUrl) { input.setCustomValidity(''); } else { input.setCustomValidity('The origin url is not valid or does not reference a code repository'); } } + +export function initTakeNewSnapshot() { + + let newSnapshotRequestAcceptedAlert = + ``; + + let newSnapshotRequestPendingAlert = + ``; + + let newSnapshotRequestRejectedAlert = + ``; + + $(document).ready(() => { + $('#swh-take-new-snapshot-form').submit(event => { + event.preventDefault(); + event.stopPropagation(); + + let originType = $('#swh-input-origin-type').val(); + let originUrl = $('#swh-input-origin-url').val(); + + originSaveRequest(originType, originUrl, + () => $('#swh-take-new-snapshot-request-status').html(newSnapshotRequestAcceptedAlert), + () => $('#swh-take-new-snapshot-request-status').html(newSnapshotRequestPendingAlert), + () => { + $('#swh-take-new-snapshot-request-status').css('color', 'red'); + $('#swh-take-new-snapshot-request-status').html(newSnapshotRequestRejectedAlert); + }); + }); + }); +} diff --git a/swh/web/browse/urls.py b/swh/web/browse/urls.py index 718bd5a03..1e04b3623 100644 --- a/swh/web/browse/urls.py +++ b/swh/web/browse/urls.py @@ -1,55 +1,53 @@ # Copyright (C) 2017-2018 The Software Heritage developers # See the AUTHORS file at the top-level directory of this distribution # License: GNU Affero General Public License version 3, or any later version # See top-level LICENSE file for more information from django.conf.urls import url from django.shortcuts import render import swh.web.browse.views.directory # noqa import swh.web.browse.views.content # noqa import swh.web.browse.views.origin_save # noqa import swh.web.browse.views.origin # noqa import swh.web.browse.views.person # noqa import swh.web.browse.views.release # noqa import swh.web.browse.views.revision # noqa import swh.web.browse.views.snapshot # noqa from swh.web.browse.browseurls import BrowseUrls from swh.web.browse.identifiers import swh_id_browse -from swh.web.config import get_config def _browse_help_view(request): return render(request, 'browse/help.html', {'heading': 'How to browse the archive ?'}) def _browse_search_view(request): return render(request, 'browse/search.html', {'heading': 'Search software origins to browse'}) def _browse_vault_view(request): return render(request, 'browse/vault-ui.html', {'heading': 'Download archive content from the Vault'}) def _browse_origin_save_view(request): return render(request, 'browse/origin-save.html', - {'heading': 'Request the saving of a software origin into the archive', # noqa - 'grecaptcha_site_key': get_config()['grecaptcha']['site_key']}) # noqa + {'heading': 'Request the saving of a software origin into the archive'}) # noqa urlpatterns = [ url(r'^$', _browse_search_view), url(r'^help/$', _browse_help_view, name='browse-help'), url(r'^search/$', _browse_search_view, name='browse-search'), url(r'^vault/$', _browse_vault_view, name='browse-vault'), url(r'^origin/save/$', _browse_origin_save_view, name='browse-origin-save'), # for backward compatibility url(r'^(?Pswh:[0-9]+:[a-z]+:[0-9a-f]+.*)/$', swh_id_browse) ] urlpatterns += BrowseUrls.get_url_patterns() diff --git a/swh/web/common/swh_templatetags.py b/swh/web/common/swh_templatetags.py index 3e652d0cd..f987a6ad4 100644 --- a/swh/web/common/swh_templatetags.py +++ b/swh/web/common/swh_templatetags.py @@ -1,152 +1,168 @@ # Copyright (C) 2017-2018 The Software Heritage developers # See the AUTHORS file at the top-level directory of this distribution # License: GNU Affero General Public License version 3, or any later version # See top-level LICENSE file for more information import json import re from django import template from django.core.serializers.json import DjangoJSONEncoder from django.utils.safestring import mark_safe from docutils.core import publish_parts from docutils.writers.html4css1 import Writer, HTMLTranslator from inspect import cleandoc +from swh.web.common.origin_save import get_savable_origin_types + register = template.Library() class NoHeaderHTMLTranslator(HTMLTranslator): """ Docutils translator subclass to customize the generation of HTML from reST-formatted docstrings """ def __init__(self, document): super().__init__(document) self.body_prefix = [] self.body_suffix = [] def visit_bullet_list(self, node): self.context.append((self.compact_simple, self.compact_p)) self.compact_p = None self.compact_simple = self.is_compactable(node) self.body.append(self.starttag(node, 'ul', CLASS='docstring')) DOCSTRING_WRITER = Writer() DOCSTRING_WRITER.translator_class = NoHeaderHTMLTranslator @register.filter def safe_docstring_display(docstring): """ Utility function to htmlize reST-formatted documentation in browsable api. """ docstring = cleandoc(docstring) return publish_parts(docstring, writer=DOCSTRING_WRITER)['html_body'] @register.filter def urlize_links_and_mails(text): """Utility function for decorating api links in browsable api. Args: text: whose content matching links should be transformed into contextual API or Browse html links. Returns The text transformed if any link is found. The text as is otherwise. """ if 'href="' not in text: text = re.sub(r'(/api/[^"<]*|/browse/[^"<]*|http.*$)', r'\1', text) return re.sub(r'([^ <>"]+@[^ <>"]+)', r'\1', text) else: return text @register.filter def urlize_header_links(text): """Utility function for decorating headers links in browsable api. Args text: Text whose content contains Link header value Returns: The text transformed with html link if any link is found. The text as is otherwise. """ links = text.split(',') ret = '' for i, link in enumerate(links): ret += re.sub(r'<(/api/.*|/browse/.*)>', r'<\1>', link) # add one link per line and align them if i != len(links) - 1: ret += '\n ' return ret @register.filter def jsonify(obj): """Utility function for converting a django template variable to JSON in order to use it in script tags. Args obj: Any django template context variable Returns: JSON representation of the variable. """ return mark_safe(json.dumps(obj, cls=DjangoJSONEncoder)) @register.filter def sub(value, arg): """Django template filter for subtracting two numbers Args: value (int/float): the value to subtract from arg (int/float): the value to subtract to Returns: int/float: The subtraction result """ return value - arg @register.filter def mul(value, arg): """Django template filter for multiplying two numbers Args: value (int/float): the value to multiply from arg (int/float): the value to multiply with Returns: int/float: The multiplication result """ return value * arg @register.filter def key_value(dict, key): """Django template filter to get a value in a dictionary. Args: dict (dict): a dictionary key (str): the key to lookup value Returns: The requested value in the dictionary """ return dict[key] + + +@register.filter +def origin_type_savable(origin_type): + """Django template filter to check if a save request can be + created for a given origin type. + + Args: + origin_type (str): the type of software origin + + Returns: + If the origin type is savable or not + """ + return origin_type in get_savable_origin_types() diff --git a/swh/web/common/utils.py b/swh/web/common/utils.py index 1522fcf3b..9362001e8 100644 --- a/swh/web/common/utils.py +++ b/swh/web/common/utils.py @@ -1,348 +1,350 @@ # Copyright (C) 2017-2018 The Software Heritage developers # See the AUTHORS file at the top-level directory of this distribution # License: GNU Affero General Public License version 3, or any later version # See top-level LICENSE file for more information import docutils.parsers.rst import docutils.utils import re import requests from datetime import datetime, timezone from dateutil import parser as date_parser from dateutil import tz from django.urls import reverse as django_reverse from django.http import QueryDict from swh.model.exceptions import ValidationError from swh.model.identifiers import ( persistent_identifier, parse_persistent_identifier, CONTENT, DIRECTORY, RELEASE, REVISION, SNAPSHOT ) from swh.web.common.exc import BadInputExc from swh.web.config import get_config swh_object_icons = { 'branch': 'fa fa-code-fork', 'branches': 'fa fa-code-fork', 'content': 'fa fa-file-text', 'directory': 'fa fa-folder', 'person': 'fa fa-user', 'revisions history': 'fa fa-history', 'release': 'fa fa-tag', 'releases': 'fa fa-tag', 'revision': 'octicon octicon-git-commit', 'snapshot': 'fa fa-camera', 'visits': 'fa fa-calendar', } def reverse(viewname, url_args=None, query_params=None, current_app=None, urlconf=None): """An override of django reverse function supporting query parameters. Args: viewname (str): the name of the django view from which to compute a url url_args (dict): dictionary of url arguments indexed by their names query_params (dict): dictionary of query parameters to append to the reversed url current_app (str): the name of the django app tighten to the view urlconf (str): url configuration module Returns: str: the url of the requested view with processed arguments and query parameters """ if url_args: url_args = {k: v for k, v in url_args.items() if v is not None} url = django_reverse(viewname, urlconf=urlconf, kwargs=url_args, current_app=current_app) if query_params: query_params = {k: v for k, v in query_params.items() if v} if query_params and len(query_params) > 0: query_dict = QueryDict('', mutable=True) for k in sorted(query_params.keys()): query_dict[k] = query_params[k] url += ('?' + query_dict.urlencode(safe='/;:')) return url def datetime_to_utc(date): """Returns datetime in UTC without timezone info Args: date (datetime.datetime): input datetime with timezone info Returns: datetime.datetime: datetime in UTC without timezone info """ if date.tzinfo: return date.astimezone(tz.gettz('UTC')).replace(tzinfo=timezone.utc) else: return date def parse_timestamp(timestamp): """Given a time or timestamp (as string), parse the result as UTC datetime. Returns: datetime.datetime: a timezone-aware datetime representing the parsed value or None if the parsing fails. Samples: - 2016-01-12 - 2016-01-12T09:19:12+0100 - Today is January 1, 2047 at 8:21:00AM - 1452591542 """ if not timestamp: return None try: date = date_parser.parse(timestamp, ignoretz=False, fuzzy=True) return datetime_to_utc(date) except Exception: try: return datetime.utcfromtimestamp(float(timestamp)).replace( tzinfo=timezone.utc) except (ValueError, OverflowError) as e: raise BadInputExc(e) def shorten_path(path): """Shorten the given path: for each hash present, only return the first 8 characters followed by an ellipsis""" sha256_re = r'([0-9a-f]{8})[0-9a-z]{56}' sha1_re = r'([0-9a-f]{8})[0-9a-f]{32}' ret = re.sub(sha256_re, r'\1...', path) return re.sub(sha1_re, r'\1...', ret) def format_utc_iso_date(iso_date, fmt='%d %B %Y, %H:%M UTC'): """Turns a string representation of an ISO 8601 date string to UTC and format it into a more human readable one. For instance, from the following input string: '2017-05-04T13:27:13+02:00' the following one is returned: '04 May 2017, 11:27 UTC'. Custom format string may also be provided as parameter Args: iso_date (str): a string representation of an ISO 8601 date fmt (str): optional date formatting string Returns: str: a formatted string representation of the input iso date """ if not iso_date: return iso_date date = parse_timestamp(iso_date) return date.strftime(fmt) def gen_path_info(path): """Function to generate path data navigation for use with a breadcrumb in the swh web ui. For instance, from a path /folder1/folder2/folder3, it returns the following list:: [{'name': 'folder1', 'path': 'folder1'}, {'name': 'folder2', 'path': 'folder1/folder2'}, {'name': 'folder3', 'path': 'folder1/folder2/folder3'}] Args: path: a filesystem path Returns: list: a list of path data for navigation as illustrated above. """ path_info = [] if path: sub_paths = path.strip('/').split('/') path_from_root = '' for p in sub_paths: path_from_root += '/' + p path_info.append({'name': p, 'path': path_from_root.strip('/')}) return path_info def get_swh_persistent_id(object_type, object_id, scheme_version=1): """ Returns the persistent identifier for a swh object based on: * the object type * the object id * the swh identifiers scheme version Args: object_type (str): the swh object type (content/directory/release/revision/snapshot) object_id (str): the swh object id (hexadecimal representation of its hash value) scheme_version (int): the scheme version of the swh persistent identifiers Returns: str: the swh object persistent identifier Raises: BadInputExc: if the provided parameters do not enable to generate a valid identifier """ try: swh_id = persistent_identifier(object_type, object_id, scheme_version) except ValidationError as e: raise BadInputExc('Invalid object (%s) for swh persistent id. %s' % (object_id, e)) else: return swh_id def resolve_swh_persistent_id(swh_id, query_params=None): """ Try to resolve a Software Heritage persistent id into an url for browsing the pointed object. Args: swh_id (str): a Software Heritage persistent identifier query_params (django.http.QueryDict): optional dict filled with query parameters to append to the browse url Returns: dict: a dict with the following keys: * **swh_id_parsed (swh.model.identifiers.PersistentId)**: the parsed identifier * **browse_url (str)**: the url for browsing the pointed object Raises: BadInputExc: if the provided identifier can not be parsed """ # noqa try: swh_id_parsed = parse_persistent_identifier(swh_id) object_type = swh_id_parsed.object_type object_id = swh_id_parsed.object_id browse_url = None query_dict = QueryDict('', mutable=True) if query_params and len(query_params) > 0: for k in sorted(query_params.keys()): query_dict[k] = query_params[k] if 'origin' in swh_id_parsed.metadata: query_dict['origin'] = swh_id_parsed.metadata['origin'] if object_type == CONTENT: query_string = 'sha1_git:' + object_id fragment = '' if 'lines' in swh_id_parsed.metadata: lines = swh_id_parsed.metadata['lines'].split('-') fragment += '#L' + lines[0] if len(lines) > 1: fragment += '-L' + lines[1] browse_url = reverse('browse-content', url_args={'query_string': query_string}, query_params=query_dict) + fragment elif object_type == DIRECTORY: browse_url = reverse('browse-directory', url_args={'sha1_git': object_id}, query_params=query_dict) elif object_type == RELEASE: browse_url = reverse('browse-release', url_args={'sha1_git': object_id}, query_params=query_dict) elif object_type == REVISION: browse_url = reverse('browse-revision', url_args={'sha1_git': object_id}, query_params=query_dict) elif object_type == SNAPSHOT: browse_url = reverse('browse-snapshot', url_args={'snapshot_id': object_id}, query_params=query_dict) except ValidationError as ve: raise BadInputExc('Error when parsing identifier. %s' % ' '.join(ve.messages)) else: return {'swh_id_parsed': swh_id_parsed, 'browse_url': browse_url} def parse_rst(text, report_level=2): """ Parse a reStructuredText string with docutils. Args: text (str): string with reStructuredText markups in it report_level (int): level of docutils report messages to print (1 info 2 warning 3 error 4 severe 5 none) Returns: docutils.nodes.document: a parsed docutils document """ parser = docutils.parsers.rst.Parser() components = (docutils.parsers.rst.Parser,) settings = docutils.frontend.OptionParser( components=components).get_default_values() settings.report_level = report_level document = docutils.utils.new_document('rst-doc', settings=settings) parser.parse(text, document) return document def get_client_ip(request): """ Return the client IP address from an incoming HTTP request. Args: request (django.http.HttpRequest): the incoming HTTP request Returns: str: The client IP address """ x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR') if x_forwarded_for: ip = x_forwarded_for.split(',')[0] else: ip = request.META.get('REMOTE_ADDR') return ip def is_recaptcha_valid(request, recaptcha_response): """ Verify if the response for Google reCAPTCHA is valid. Args: request (django.http.HttpRequest): the incoming HTTP request recaptcha_response (str): the reCAPTCHA response Returns: bool: Whether the reCAPTCHA response is valid or not """ config = get_config() return requests.post( config['grecaptcha']['validation_url'], data={ 'secret': config['grecaptcha']['private_key'], 'response': recaptcha_response, 'remoteip': get_client_ip(request) }, verify=True ).json().get("success", False) def context_processor(request): """ Django context processor used to inject variables in all swh-web templates. """ - return {'swh_object_icons': swh_object_icons} + config = get_config() + return {'swh_object_icons': swh_object_icons, + 'grecaptcha_site_key': config['grecaptcha']['site_key']} diff --git a/swh/web/templates/browse/layout.html b/swh/web/templates/browse/layout.html index d922de2d5..7966a659f 100644 --- a/swh/web/templates/browse/layout.html +++ b/swh/web/templates/browse/layout.html @@ -1,23 +1,24 @@ {% extends "layout.html" %} {% comment %} Copyright (C) 2017-2018 The Software Heritage developers See the AUTHORS file at the top-level directory of this distribution License: GNU Affero General Public License version 3, or any later version See top-level LICENSE file for more information {% endcomment %} {% load swh_templatetags %} {% load render_bundle from webpack_loader %} {% block title %}{{ heading }} – Software Heritage archive {% endblock %} {% block header %} {% render_bundle 'browse' %} {% render_bundle 'vault' %} + {% endblock %} {% block content %}
Beta version
{% block browse-content %}{% endblock %} {% endblock %} diff --git a/swh/web/templates/browse/origin-save.html b/swh/web/templates/browse/origin-save.html index 1988b269b..96c73bb2c 100644 --- a/swh/web/templates/browse/origin-save.html +++ b/swh/web/templates/browse/origin-save.html @@ -1,116 +1,111 @@ {% extends "./layout.html" %} {% comment %} Copyright (C) 2018 The Software Heritage developers See the AUTHORS file at the top-level directory of this distribution License: GNU Affero General Public License version 3, or any later version See top-level LICENSE file for more information {% endcomment %} -{% block header %} -{{ block.super }} - -{% endblock %} - {% block navbar-content %}

Save code now

{% endblock %} {% block browse-content %}

You can contribute to extend the content of the Software Heritage archive by submitting an origin save request. To do so, fill the required info in the form below:

  • Origin type: the type of version control system the software origin is using.
    Currently, the only supported type is git, for origins using Git.
    Soon, the following origin types will also be available to save into the archive:
  • Origin url: the url of the remote repository for the software origin.
    In order to avoid saving errors from Software Heritage, you should provide the clone/checkout url as given by the provider hosting the software origin.
    It can easily be found in the web interface used to browse the software origin.
    For instance, if you want to save a git origin into the archive, you should check that the command $ git clone <origin_url>
    does not return an error before submitting a request.

Once submitted, your save request can either be:

  • accepted: a visit to the provided origin will then be scheduled by Software Heritage in order to load its content into the archive as soon as possible
  • rejected: the provided origin url is blacklisted and no visit will be scheduled
  • put in pending state: a manual review will then be performed in order to determine if the origin can be safely loaded or not into the archive

Once a save request has been accepted, you can follow its current status in the submitted save requests list.

{% csrf_token %}
The origin type must be specified
The origin url is not valid or does not reference a code repository
Request date Origin type Origin url Request status Save task status
{% endblock %} \ No newline at end of file diff --git a/swh/web/templates/includes/take-new-snapshot.html b/swh/web/templates/includes/take-new-snapshot.html new file mode 100644 index 000000000..555d5b3c6 --- /dev/null +++ b/swh/web/templates/includes/take-new-snapshot.html @@ -0,0 +1,73 @@ +{% comment %} +Copyright (C) 2019 The Software Heritage developers +See the AUTHORS file at the top-level directory of this distribution +License: GNU Affero General Public License version 3, or any later version +See top-level LICENSE file for more information +{% endcomment %} + +{% load swh_templatetags %} + +{% if snapshot_context and snapshot_context.origin_info and snapshot_context.origin_info.type|origin_type_savable %} + + + + + + + + + +{% endif %} \ No newline at end of file diff --git a/swh/web/templates/includes/top-navigation.html b/swh/web/templates/includes/top-navigation.html index 5966fe08b..c3daf6e09 100644 --- a/swh/web/templates/includes/top-navigation.html +++ b/swh/web/templates/includes/top-navigation.html @@ -1,125 +1,126 @@ {% comment %} Copyright (C) 2017-2018 The Software Heritage developers See the AUTHORS file at the top-level directory of this distribution License: GNU Affero General Public License version 3, or any later version See top-level LICENSE file for more information {% endcomment %} {% load swh_templatetags %}
{% if snapshot_context %} {% if snapshot_context.branch or snapshot_context.release %} {% endif %} {% endif %}
{% if top_right_link %} {% if top_right_link.icon %} {% endif %} {{ top_right_link.text }} {% endif %} {% if show_actions_menu %} {% endif %}
{% include "includes/breadcrumbs.html" %}
{% include "includes/show-swh-ids.html" %} diff --git a/swh/web/tests/browse/views/test_origin.py b/swh/web/tests/browse/views/test_origin.py index 987c4fc2e..4425c196c 100644 --- a/swh/web/tests/browse/views/test_origin.py +++ b/swh/web/tests/browse/views/test_origin.py @@ -1,914 +1,919 @@ # Copyright (C) 2017-2018 The Software Heritage developers # See the AUTHORS file at the top-level directory of this distribution # License: GNU Affero General Public License version 3, or any later version # See top-level LICENSE file for more information # flake8: noqa from unittest.mock import patch from django.utils.html import escape from swh.web.common.exc import NotFoundExc from swh.web.common.utils import ( reverse, gen_path_info, format_utc_iso_date, parse_timestamp, get_swh_persistent_id ) from swh.web.tests.testcase import WebTestCase from .data.origin_test_data import ( origin_info_test_data, origin_visits_test_data, stub_content_origin_info, stub_content_origin_visit_id, stub_content_origin_visit_unix_ts, stub_content_origin_visit_iso_date, stub_content_origin_branch, stub_content_origin_visits, stub_content_origin_snapshot, stub_origin_info, stub_visit_id, stub_origin_visits, stub_origin_snapshot, stub_origin_root_directory_entries, stub_origin_master_branch, stub_origin_root_directory_sha1, stub_origin_sub_directory_path, stub_origin_sub_directory_entries, stub_visit_unix_ts, stub_visit_iso_date ) from .data.content_test_data import ( stub_content_root_dir, stub_content_text_data, stub_content_text_path ) stub_origin_info_no_type = dict(stub_origin_info) stub_origin_info_no_type['type'] = None def _to_snapshot_dict(branches=None, releases=None): snp = {'branches': {}} if branches: for b in branches: snp['branches'][b['name']] = { 'target': b['revision'], 'target_type': 'revision' } if releases: for r in releases: snp['branches'][r['branch_name']] = { 'target': r['id'], 'target_type': 'release' } return snp class SwhBrowseOriginTest(WebTestCase): @patch('swh.web.browse.utils.service') @patch('swh.web.browse.utils.get_origin_visit_snapshot') @patch('swh.web.common.origin_visits.get_origin_visits') @patch('swh.web.browse.utils.get_origin_info') @patch('swh.web.browse.views.origin.get_origin_info') @patch('swh.web.browse.views.origin.get_origin_visits') @patch('swh.web.browse.views.origin.service') def test_origin_visits_browse(self, mock_service, mock_get_origin_visits, mock_get_origin_info, mock_get_origin_info_utils, mock_get_origin_visits_utils, mock_get_origin_visit_snapshot, mock_utils_service): mock_service.lookup_origin.return_value = origin_info_test_data mock_get_origin_info.return_value = origin_info_test_data mock_get_origin_info_utils.return_value = origin_info_test_data mock_get_origin_visits.return_value = origin_visits_test_data mock_get_origin_visits_utils.return_value = origin_visits_test_data mock_get_origin_visit_snapshot.return_value = stub_content_origin_snapshot mock_utils_service.lookup_snapshot_size.return_value = { 'revision': len(stub_content_origin_snapshot[0]), 'release': len(stub_content_origin_snapshot[1]) } url = reverse('browse-origin-visits', url_args={'origin_type': origin_info_test_data['type'], 'origin_url': origin_info_test_data['url']}) resp = self.client.get(url) self.assertEqual(resp.status_code, 200) self.assertTemplateUsed('origin-visits.html') url = reverse('browse-origin-visits', url_args={'origin_url': origin_info_test_data['url']}) resp = self.client.get(url) self.assertEqual(resp.status_code, 200) self.assertTemplateUsed('origin-visits.html') def origin_content_view_helper(self, origin_info, origin_visits, origin_branches, origin_releases, origin_branch, root_dir_sha1, content_sha1, content_sha1_git, content_path, content_data, content_language, visit_id=None, timestamp=None): url_args = {'origin_type': origin_info['type'], 'origin_url': origin_info['url'], 'path': content_path} if not visit_id: visit_id = origin_visits[-1]['visit'] query_params = {} if timestamp: url_args['timestamp'] = timestamp if visit_id: query_params['visit_id'] = visit_id url = reverse('browse-origin-content', url_args=url_args, query_params=query_params) resp = self.client.get(url) self.assertEqual(resp.status_code, 200) self.assertTemplateUsed('content.html') self.assertContains(resp, '' % content_language) self.assertContains(resp, escape(content_data)) split_path = content_path.split('/') filename = split_path[-1] path = content_path.replace(filename, '')[:-1] path_info = gen_path_info(path) del url_args['path'] if timestamp: url_args['timestamp'] = \ format_utc_iso_date(parse_timestamp(timestamp).isoformat(), '%Y-%m-%dT%H:%M:%S') root_dir_url = reverse('browse-origin-directory', url_args=url_args, query_params=query_params) self.assertContains(resp, '
  • ', count=len(path_info)+1) self.assertContains(resp, '%s' % (root_dir_url, root_dir_sha1[:7])) for p in path_info: url_args['path'] = p['path'] dir_url = reverse('browse-origin-directory', url_args=url_args, query_params=query_params) self.assertContains(resp, '%s' % (dir_url, p['name'])) self.assertContains(resp, '
  • %s
  • ' % filename) query_string = 'sha1_git:' + content_sha1 url_raw = reverse('browse-content-raw', url_args={'query_string': query_string}, query_params={'filename': filename}) self.assertContains(resp, url_raw) del url_args['path'] origin_branches_url = \ reverse('browse-origin-branches', url_args=url_args, query_params=query_params) self.assertContains(resp, 'Branches (%s)' % (origin_branches_url, len(origin_branches))) origin_releases_url = \ reverse('browse-origin-releases', url_args=url_args, query_params=query_params) self.assertContains(resp, 'Releases (%s)' % (origin_releases_url, len(origin_releases))) self.assertContains(resp, '
  • ', count=len(origin_branches)) url_args['path'] = content_path for branch in origin_branches: query_params['branch'] = branch['name'] root_dir_branch_url = \ reverse('browse-origin-content', url_args=url_args, query_params=query_params) self.assertContains(resp, '' % root_dir_branch_url) self.assertContains(resp, '
  • ', count=len(origin_releases)) query_params['branch'] = None for release in origin_releases: query_params['release'] = release['name'] root_dir_release_url = \ reverse('browse-origin-content', url_args=url_args, query_params=query_params) self.assertContains(resp, '' % root_dir_release_url) del url_args['origin_type'] url = reverse('browse-origin-content', url_args=url_args, query_params=query_params) resp = self.client.get(url) self.assertEqual(resp.status_code, 200) self.assertTemplateUsed('content.html') swh_cnt_id = get_swh_persistent_id('content', content_sha1_git) swh_cnt_id_url = reverse('browse-swh-id', url_args={'swh_id': swh_cnt_id}) self.assertContains(resp, swh_cnt_id) self.assertContains(resp, swh_cnt_id_url) + self.assertContains(resp, 'swh-take-new-snapshot') + @patch('swh.web.common.origin_visits.get_origin_visits') @patch('swh.web.browse.utils.get_origin_visit_snapshot') @patch('swh.web.browse.views.utils.snapshot_context.service') @patch('swh.web.browse.utils.service') @patch('swh.web.browse.views.utils.snapshot_context.request_content') def test_origin_content_view(self, mock_request_content, mock_utils_service, mock_service, mock_get_origin_visit_snapshot, mock_get_origin_visits): stub_content_text_sha1 = stub_content_text_data['checksums']['sha1'] stub_content_text_sha1_git = stub_content_text_data['checksums']['sha1_git'] mock_get_origin_visits.return_value = stub_content_origin_visits mock_get_origin_visit_snapshot.return_value = stub_content_origin_snapshot mock_service.lookup_directory_with_path.return_value = \ {'target': stub_content_text_sha1} mock_request_content.return_value = stub_content_text_data mock_utils_service.lookup_origin.return_value = stub_content_origin_info mock_utils_service.lookup_snapshot_size.return_value = { 'revision': len(stub_content_origin_snapshot[0]), 'release': len(stub_content_origin_snapshot[1]) } self.origin_content_view_helper(stub_content_origin_info, stub_content_origin_visits, stub_content_origin_snapshot[0], stub_content_origin_snapshot[1], stub_content_origin_branch, stub_content_root_dir, stub_content_text_sha1, stub_content_text_sha1_git, stub_content_text_path, stub_content_text_data['raw_data'], 'cpp') self.origin_content_view_helper(stub_content_origin_info, stub_content_origin_visits, stub_content_origin_snapshot[0], stub_content_origin_snapshot[1], stub_content_origin_branch, stub_content_root_dir, stub_content_text_sha1, stub_content_text_sha1_git, stub_content_text_path, stub_content_text_data['raw_data'], 'cpp', visit_id=stub_content_origin_visit_id) self.origin_content_view_helper(stub_content_origin_info, stub_content_origin_visits, stub_content_origin_snapshot[0], stub_content_origin_snapshot[1], stub_content_origin_branch, stub_content_root_dir, stub_content_text_sha1, stub_content_text_sha1_git, stub_content_text_path, stub_content_text_data['raw_data'], 'cpp', timestamp=stub_content_origin_visit_unix_ts) self.origin_content_view_helper(stub_content_origin_info, stub_content_origin_visits, stub_content_origin_snapshot[0], stub_content_origin_snapshot[1], stub_content_origin_branch, stub_content_root_dir, stub_content_text_sha1, stub_content_text_sha1_git, stub_content_text_path, stub_content_text_data['raw_data'], 'cpp', timestamp=stub_content_origin_visit_iso_date) def origin_directory_view_helper(self, origin_info, origin_visits, origin_branches, origin_releases, origin_branch, root_directory_sha1, directory_entries, visit_id=None, timestamp=None, path=None): dirs = [e for e in directory_entries if e['type'] in ('dir', 'rev')] files = [e for e in directory_entries if e['type'] == 'file'] if not visit_id: visit_id = origin_visits[-1]['visit'] url_args = {'origin_url': origin_info['url']} query_params = {} if timestamp: url_args['timestamp'] = timestamp else: query_params['visit_id'] = visit_id if path: url_args['path'] = path url = reverse('browse-origin-directory', url_args=url_args, query_params=query_params) resp = self.client.get(url) self.assertEqual(resp.status_code, 200) self.assertTemplateUsed('directory.html') self.assertEqual(resp.status_code, 200) self.assertTemplateUsed('directory.html') self.assertContains(resp, '', count=len(dirs)) self.assertContains(resp, '', count=len(files)) if timestamp: url_args['timestamp'] = \ format_utc_iso_date(parse_timestamp(timestamp).isoformat(), '%Y-%m-%dT%H:%M:%S') for d in dirs: if d['type'] == 'rev': dir_url = reverse('browse-revision', url_args={'sha1_git': d['target']}) else: dir_path = d['name'] if path: dir_path = "%s/%s" % (path, d['name']) dir_url_args = dict(url_args) dir_url_args['path'] = dir_path dir_url = reverse('browse-origin-directory', url_args=dir_url_args, query_params=query_params) self.assertContains(resp, dir_url) for f in files: file_path = f['name'] if path: file_path = "%s/%s" % (path, f['name']) file_url_args = dict(url_args) file_url_args['path'] = file_path file_url = reverse('browse-origin-content', url_args=file_url_args, query_params=query_params) self.assertContains(resp, file_url) if 'path' in url_args: del url_args['path'] root_dir_branch_url = \ reverse('browse-origin-directory', url_args=url_args, query_params=query_params) nb_bc_paths = 1 if path: nb_bc_paths = len(path.split('/')) + 1 self.assertContains(resp, '
  • ', count=nb_bc_paths) self.assertContains(resp, '%s' % (root_dir_branch_url, root_directory_sha1[:7])) origin_branches_url = \ reverse('browse-origin-branches', url_args=url_args, query_params=query_params) self.assertContains(resp, 'Branches (%s)' % (origin_branches_url, len(origin_branches))) origin_releases_url = \ reverse('browse-origin-releases', url_args=url_args, query_params=query_params) self.assertContains(resp, 'Releases (%s)' % (origin_releases_url, len(origin_releases))) if path: url_args['path'] = path self.assertContains(resp, '
  • ', count=len(origin_branches)) for branch in origin_branches: query_params['branch'] = branch['name'] root_dir_branch_url = \ reverse('browse-origin-directory', url_args=url_args, query_params=query_params) self.assertContains(resp, '' % root_dir_branch_url) self.assertContains(resp, '
  • ', count=len(origin_releases)) query_params['branch'] = None for release in origin_releases: query_params['release'] = release['name'] root_dir_release_url = \ reverse('browse-origin-directory', url_args=url_args, query_params=query_params) self.assertContains(resp, '' % root_dir_release_url) + self.assertContains(resp, 'vault-cook-directory') self.assertContains(resp, 'vault-cook-revision') swh_dir_id = get_swh_persistent_id('directory', directory_entries[0]['dir_id']) # noqa swh_dir_id_url = reverse('browse-swh-id', url_args={'swh_id': swh_dir_id}) self.assertContains(resp, swh_dir_id) self.assertContains(resp, swh_dir_id_url) + self.assertContains(resp, 'swh-take-new-snapshot') + @patch('swh.web.common.origin_visits.get_origin_visits') @patch('swh.web.browse.utils.get_origin_visit_snapshot') @patch('swh.web.browse.utils.service') @patch('swh.web.browse.views.origin.service') def test_origin_root_directory_view(self, mock_origin_service, mock_utils_service, mock_get_origin_visit_snapshot, mock_get_origin_visits): mock_get_origin_visits.return_value = stub_origin_visits mock_get_origin_visit_snapshot.return_value = stub_origin_snapshot mock_utils_service.lookup_directory.return_value = \ stub_origin_root_directory_entries mock_utils_service.lookup_origin.return_value = stub_origin_info mock_utils_service.lookup_snapshot_size.return_value = { 'revision': len(stub_origin_snapshot[0]), 'release': len(stub_origin_snapshot[1]) } self.origin_directory_view_helper(stub_origin_info, stub_origin_visits, stub_origin_snapshot[0], stub_origin_snapshot[1], stub_origin_master_branch, stub_origin_root_directory_sha1, stub_origin_root_directory_entries) self.origin_directory_view_helper(stub_origin_info, stub_origin_visits, stub_origin_snapshot[0], stub_origin_snapshot[1], stub_origin_master_branch, stub_origin_root_directory_sha1, stub_origin_root_directory_entries, visit_id=stub_visit_id) self.origin_directory_view_helper(stub_origin_info, stub_origin_visits, stub_origin_snapshot[0], stub_origin_snapshot[1], stub_origin_master_branch, stub_origin_root_directory_sha1, stub_origin_root_directory_entries, timestamp=stub_visit_unix_ts) self.origin_directory_view_helper(stub_origin_info, stub_origin_visits, stub_origin_snapshot[0], stub_origin_snapshot[1], stub_origin_master_branch, stub_origin_root_directory_sha1, stub_origin_root_directory_entries, timestamp=stub_visit_iso_date) self.origin_directory_view_helper(stub_origin_info_no_type, stub_origin_visits, stub_origin_snapshot[0], stub_origin_snapshot[1], stub_origin_master_branch, stub_origin_root_directory_sha1, stub_origin_root_directory_entries) self.origin_directory_view_helper(stub_origin_info_no_type, stub_origin_visits, stub_origin_snapshot[0], stub_origin_snapshot[1], stub_origin_master_branch, stub_origin_root_directory_sha1, stub_origin_root_directory_entries, visit_id=stub_visit_id) self.origin_directory_view_helper(stub_origin_info_no_type, stub_origin_visits, stub_origin_snapshot[0], stub_origin_snapshot[1], stub_origin_master_branch, stub_origin_root_directory_sha1, stub_origin_root_directory_entries, timestamp=stub_visit_unix_ts) self.origin_directory_view_helper(stub_origin_info_no_type, stub_origin_visits, stub_origin_snapshot[0], stub_origin_snapshot[1], stub_origin_master_branch, stub_origin_root_directory_sha1, stub_origin_root_directory_entries, timestamp=stub_visit_iso_date) @patch('swh.web.common.origin_visits.get_origin_visits') @patch('swh.web.browse.utils.get_origin_visit_snapshot') @patch('swh.web.browse.utils.service') @patch('swh.web.browse.views.utils.snapshot_context.service') def test_origin_sub_directory_view(self, mock_origin_service, mock_utils_service, mock_get_origin_visit_snapshot, mock_get_origin_visits): mock_get_origin_visits.return_value = stub_origin_visits mock_get_origin_visit_snapshot.return_value = stub_origin_snapshot mock_utils_service.lookup_directory.return_value = \ stub_origin_sub_directory_entries mock_origin_service.lookup_directory_with_path.return_value = \ {'target': stub_origin_sub_directory_entries[0]['dir_id'], 'type' : 'dir'} mock_utils_service.lookup_origin.return_value = stub_origin_info mock_utils_service.lookup_snapshot_size.return_value = { 'revision': len(stub_origin_snapshot[0]), 'release': len(stub_origin_snapshot[1]) } self.origin_directory_view_helper(stub_origin_info, stub_origin_visits, stub_origin_snapshot[0], stub_origin_snapshot[1], stub_origin_master_branch, stub_origin_root_directory_sha1, stub_origin_sub_directory_entries, path=stub_origin_sub_directory_path) self.origin_directory_view_helper(stub_origin_info, stub_origin_visits, stub_origin_snapshot[0], stub_origin_snapshot[1], stub_origin_master_branch, stub_origin_root_directory_sha1, stub_origin_sub_directory_entries, visit_id=stub_visit_id, path=stub_origin_sub_directory_path) self.origin_directory_view_helper(stub_origin_info, stub_origin_visits, stub_origin_snapshot[0], stub_origin_snapshot[1], stub_origin_master_branch, stub_origin_root_directory_sha1, stub_origin_sub_directory_entries, timestamp=stub_visit_unix_ts, path=stub_origin_sub_directory_path) self.origin_directory_view_helper(stub_origin_info, stub_origin_visits, stub_origin_snapshot[0], stub_origin_snapshot[1], stub_origin_master_branch, stub_origin_root_directory_sha1, stub_origin_sub_directory_entries, timestamp=stub_visit_iso_date, path=stub_origin_sub_directory_path) self.origin_directory_view_helper(stub_origin_info_no_type, stub_origin_visits, stub_origin_snapshot[0], stub_origin_snapshot[1], stub_origin_master_branch, stub_origin_root_directory_sha1, stub_origin_sub_directory_entries, path=stub_origin_sub_directory_path) self.origin_directory_view_helper(stub_origin_info_no_type, stub_origin_visits, stub_origin_snapshot[0], stub_origin_snapshot[1], stub_origin_master_branch, stub_origin_root_directory_sha1, stub_origin_sub_directory_entries, visit_id=stub_visit_id, path=stub_origin_sub_directory_path) self.origin_directory_view_helper(stub_origin_info_no_type, stub_origin_visits, stub_origin_snapshot[0], stub_origin_snapshot[1], stub_origin_master_branch, stub_origin_root_directory_sha1, stub_origin_sub_directory_entries, timestamp=stub_visit_unix_ts, path=stub_origin_sub_directory_path) self.origin_directory_view_helper(stub_origin_info_no_type, stub_origin_visits, stub_origin_snapshot[0], stub_origin_snapshot[1], stub_origin_master_branch, stub_origin_root_directory_sha1, stub_origin_sub_directory_entries, timestamp=stub_visit_iso_date, path=stub_origin_sub_directory_path) @patch('swh.web.browse.views.utils.snapshot_context.request_content') @patch('swh.web.common.origin_visits.get_origin_visits') @patch('swh.web.browse.utils.get_origin_visit_snapshot') @patch('swh.web.browse.utils.service') @patch('swh.web.browse.views.origin.service') @patch('swh.web.browse.views.utils.snapshot_context.service') @patch('swh.web.browse.views.origin.get_origin_info') def test_origin_request_errors(self, mock_get_origin_info, mock_snapshot_service, mock_origin_service, mock_utils_service, mock_get_origin_visit_snapshot, mock_get_origin_visits, mock_request_content): mock_get_origin_info.side_effect = \ NotFoundExc('origin not found') url = reverse('browse-origin-visits', url_args={'origin_type': 'foo', 'origin_url': 'bar'}) resp = self.client.get(url) self.assertEqual(resp.status_code, 404) self.assertTemplateUsed('error.html') self.assertContains(resp, 'origin not found', status_code=404) mock_utils_service.lookup_origin.side_effect = None mock_utils_service.lookup_origin.return_value = origin_info_test_data mock_get_origin_visits.return_value = [] url = reverse('browse-origin-directory', url_args={'origin_type': 'foo', 'origin_url': 'bar'}) resp = self.client.get(url) self.assertEqual(resp.status_code, 404) self.assertTemplateUsed('error.html') self.assertContains(resp, "No visit", status_code=404) mock_get_origin_visits.return_value = stub_origin_visits mock_get_origin_visit_snapshot.side_effect = \ NotFoundExc('visit not found') url = reverse('browse-origin-directory', url_args={'origin_type': 'foo', 'origin_url': 'bar'}, query_params={'visit_id': len(stub_origin_visits)+1}) resp = self.client.get(url) self.assertEqual(resp.status_code, 404) self.assertTemplateUsed('error.html') self.assertRegex(resp.content.decode('utf-8'), 'Visit.*not found') mock_get_origin_visits.return_value = stub_origin_visits mock_get_origin_visit_snapshot.side_effect = None mock_get_origin_visit_snapshot.return_value = stub_origin_snapshot mock_utils_service.lookup_snapshot_size.return_value = { 'revision': len(stub_origin_snapshot[0]), 'release': len(stub_origin_snapshot[1]) } mock_utils_service.lookup_directory.side_effect = \ NotFoundExc('Directory not found') url = reverse('browse-origin-directory', url_args={'origin_type': 'foo', 'origin_url': 'bar'}) resp = self.client.get(url) self.assertEqual(resp.status_code, 404) self.assertTemplateUsed('error.html') self.assertContains(resp, 'Directory not found', status_code=404) with patch('swh.web.browse.views.utils.snapshot_context.get_snapshot_context') \ as mock_get_snapshot_context: mock_get_snapshot_context.side_effect = \ NotFoundExc('Snapshot not found') url = reverse('browse-origin-directory', url_args={'origin_type': 'foo', 'origin_url': 'bar'}) resp = self.client.get(url) self.assertEqual(resp.status_code, 404) self.assertTemplateUsed('error.html') self.assertContains(resp, 'Snapshot not found', status_code=404) mock_origin_service.lookup_origin.side_effect = None mock_origin_service.lookup_origin.return_value = origin_info_test_data mock_get_origin_visits.return_value = [] url = reverse('browse-origin-content', url_args={'origin_type': 'foo', 'origin_url': 'bar', 'path': 'foo'}) resp = self.client.get(url) self.assertEqual(resp.status_code, 404) self.assertTemplateUsed('error.html') self.assertContains(resp, "No visit", status_code=404) mock_get_origin_visits.return_value = stub_origin_visits mock_get_origin_visit_snapshot.side_effect = \ NotFoundExc('visit not found') url = reverse('browse-origin-content', url_args={'origin_type': 'foo', 'origin_url': 'bar', 'path': 'foo'}, query_params={'visit_id': len(stub_origin_visits)+1}) resp = self.client.get(url) self.assertEqual(resp.status_code, 404) self.assertTemplateUsed('error.html') self.assertRegex(resp.content.decode('utf-8'), 'Visit.*not found') mock_get_origin_visits.return_value = stub_origin_visits mock_get_origin_visit_snapshot.side_effect = None mock_get_origin_visit_snapshot.return_value = ([], []) url = reverse('browse-origin-content', url_args={'origin_type': 'foo', 'origin_url': 'bar', 'path': 'baz'}) resp = self.client.get(url) self.assertEqual(resp.status_code, 404) self.assertTemplateUsed('error.html') self.assertRegex(resp.content.decode('utf-8'), 'Origin.*has an empty list of branches') mock_get_origin_visit_snapshot.return_value = stub_origin_snapshot mock_snapshot_service.lookup_directory_with_path.return_value = \ {'target': stub_content_text_data['checksums']['sha1']} mock_request_content.side_effect = \ NotFoundExc('Content not found') url = reverse('browse-origin-content', url_args={'origin_type': 'foo', 'origin_url': 'bar', 'path': 'baz'}) resp = self.client.get(url) self.assertEqual(resp.status_code, 404) self.assertTemplateUsed('error.html') self.assertContains(resp, 'Content not found', status_code=404) @patch('swh.web.common.origin_visits.get_origin_visits') @patch('swh.web.browse.utils.get_origin_visit_snapshot') @patch('swh.web.browse.utils.service') def test_origin_empty_snapshot(self, mock_utils_service, mock_get_origin_visit_snapshot, mock_get_origin_visits): mock_get_origin_visits.return_value = stub_origin_visits mock_get_origin_visit_snapshot.return_value = ([], []) mock_utils_service.lookup_snapshot_size.return_value = { 'revision': 0, 'release': 0 } url = reverse('browse-origin-directory', url_args={'origin_type': 'foo', 'origin_url': 'bar'}) resp = self.client.get(url) self.assertEqual(resp.status_code, 200) self.assertTemplateUsed('content.html') self.assertRegex(resp.content.decode('utf-8'), 'snapshot.*is empty') def origin_branches_helper(self, origin_info, origin_snapshot): url_args = {'origin_type': origin_info['type'], 'origin_url': origin_info['url']} url = reverse('browse-origin-branches', url_args=url_args) resp = self.client.get(url) self.assertEqual(resp.status_code, 200) self.assertTemplateUsed('branches.html') origin_branches = origin_snapshot[0] origin_releases = origin_snapshot[1] origin_branches_url = \ reverse('browse-origin-branches', url_args=url_args) self.assertContains(resp, 'Branches (%s)' % (origin_branches_url, len(origin_branches))) origin_releases_url = \ reverse('browse-origin-releases', url_args=url_args) self.assertContains(resp, 'Releases (%s)' % (origin_releases_url, len(origin_releases))) self.assertContains(resp, '' % escape(browse_branch_url)) browse_revision_url = reverse('browse-revision', url_args={'sha1_git': branch['revision']}, query_params={'origin_type': origin_info['type'], 'origin': origin_info['url']}) self.assertContains(resp, '' % escape(browse_revision_url)) @patch('swh.web.browse.views.utils.snapshot_context.process_snapshot_branches') @patch('swh.web.browse.views.utils.snapshot_context.service') @patch('swh.web.common.origin_visits.get_origin_visits') @patch('swh.web.browse.utils.get_origin_visit_snapshot') @patch('swh.web.browse.utils.service') @patch('swh.web.browse.views.origin.service') def test_origin_branches(self, mock_origin_service, mock_utils_service, mock_get_origin_visit_snapshot, mock_get_origin_visits, mock_snp_ctx_service, mock_snp_ctx_process_branches): mock_get_origin_visits.return_value = stub_origin_visits mock_get_origin_visit_snapshot.return_value = stub_origin_snapshot mock_utils_service.lookup_origin.return_value = stub_origin_info mock_utils_service.lookup_snapshot_size.return_value = \ {'revision': len(stub_origin_snapshot[0]), 'release': len(stub_origin_snapshot[1])} mock_snp_ctx_service.lookup_snapshot.return_value = \ _to_snapshot_dict(branches=stub_origin_snapshot[0]) mock_snp_ctx_process_branches.return_value = stub_origin_snapshot self.origin_branches_helper(stub_origin_info, stub_origin_snapshot) self.origin_branches_helper(stub_origin_info_no_type, stub_origin_snapshot) def origin_releases_helper(self, origin_info, origin_snapshot): url_args = {'origin_type': origin_info['type'], 'origin_url': origin_info['url']} url = reverse('browse-origin-releases', url_args=url_args) resp = self.client.get(url) self.assertEqual(resp.status_code, 200) self.assertTemplateUsed('releases.html') origin_branches = origin_snapshot[0] origin_releases = origin_snapshot[1] origin_branches_url = \ reverse('browse-origin-branches', url_args=url_args) self.assertContains(resp, 'Branches (%s)' % (origin_branches_url, len(origin_branches))) origin_releases_url = \ reverse('browse-origin-releases', url_args=url_args) self.assertContains(resp, 'Releases (%s)' % (origin_releases_url, len(origin_releases))) self.assertContains(resp, '' % escape(browse_release_url)) self.assertContains(resp, '' % escape(browse_revision_url)) @patch('swh.web.browse.views.utils.snapshot_context.process_snapshot_branches') @patch('swh.web.browse.views.utils.snapshot_context.service') @patch('swh.web.common.origin_visits.get_origin_visits') @patch('swh.web.browse.utils.get_origin_visit_snapshot') @patch('swh.web.browse.utils.service') @patch('swh.web.browse.views.origin.service') def test_origin_releases(self, mock_origin_service, mock_utils_service, mock_get_origin_visit_snapshot, mock_get_origin_visits, mock_snp_ctx_service, mock_snp_ctx_process_branches): mock_get_origin_visits.return_value = stub_origin_visits mock_get_origin_visit_snapshot.return_value = stub_origin_snapshot mock_utils_service.lookup_origin.return_value = stub_origin_info mock_utils_service.lookup_snapshot_size.return_value = \ {'revision': len(stub_origin_snapshot[0]), 'release': len(stub_origin_snapshot[1])} mock_snp_ctx_service.lookup_snapshot.return_value = \ _to_snapshot_dict(releases=stub_origin_snapshot[1]) mock_snp_ctx_process_branches.return_value = stub_origin_snapshot self.origin_releases_helper(stub_origin_info, stub_origin_snapshot) self.origin_releases_helper(stub_origin_info_no_type, stub_origin_snapshot) diff --git a/swh/web/tests/browse/views/test_revision.py b/swh/web/tests/browse/views/test_revision.py index 69e58991f..444d0122a 100644 --- a/swh/web/tests/browse/views/test_revision.py +++ b/swh/web/tests/browse/views/test_revision.py @@ -1,255 +1,257 @@ # Copyright (C) 2017-2018 The Software Heritage developers # See the AUTHORS file at the top-level directory of this distribution # License: GNU Affero General Public License version 3, or any later version # See top-level LICENSE file for more information # flake8: noqa from unittest.mock import patch, MagicMock from django.utils.html import escape from swh.web.common.exc import NotFoundExc from swh.web.common.utils import ( reverse, format_utc_iso_date, get_swh_persistent_id, parse_timestamp ) from swh.web.tests.testcase import WebTestCase from .data.revision_test_data import ( revision_id_test, revision_metadata_test, revision_history_log_test ) from .data.origin_test_data import stub_origin_visits, stub_origin_snapshot class SwhBrowseRevisionTest(WebTestCase): @patch('swh.web.browse.utils.get_origin_visit_snapshot') @patch('swh.web.browse.views.revision.service') @patch('swh.web.browse.utils.service') @patch('swh.web.common.origin_visits.get_origin_visits') def test_revision_browse(self, mock_get_origin_visits, mock_service_utils, mock_service, mock_get_origin_visit_snapshot): mock_service.lookup_revision.return_value = revision_metadata_test url = reverse('browse-revision', url_args={'sha1_git': revision_id_test}) author_id = revision_metadata_test['author']['id'] author_name = revision_metadata_test['author']['name'] committer_id = revision_metadata_test['committer']['id'] committer_name = revision_metadata_test['committer']['name'] dir_id = revision_metadata_test['directory'] author_url = reverse('browse-person', url_args={'person_id': author_id}) committer_url = reverse('browse-person', url_args={'person_id': committer_id}) directory_url = reverse('browse-directory', url_args={'sha1_git': dir_id}) history_url = reverse('browse-revision-log', url_args={'sha1_git': revision_id_test}) resp = self.client.get(url) self.assertEqual(resp.status_code, 200) self.assertTemplateUsed('browse/revision.html') self.assertContains(resp, '%s' % (author_url, author_name)) self.assertContains(resp, '%s' % (committer_url, committer_name)) self.assertContains(resp, directory_url) self.assertContains(resp, history_url) for parent in revision_metadata_test['parents']: parent_url = reverse('browse-revision', url_args={'sha1_git': parent}) self.assertContains(resp, '%s' % (parent_url, parent)) author_date = revision_metadata_test['date'] committer_date = revision_metadata_test['committer_date'] message_lines = revision_metadata_test['message'].split('\n') self.assertContains(resp, format_utc_iso_date(author_date)) self.assertContains(resp, format_utc_iso_date(committer_date)) self.assertContains(resp, message_lines[0]) self.assertContains(resp, '\n'.join(message_lines[1:])) origin_info = { 'id': '7416001', 'type': 'git', 'url': 'https://github.com/webpack/webpack' } mock_service_utils.lookup_origin.return_value = origin_info mock_get_origin_visits.return_value = stub_origin_visits mock_get_origin_visit_snapshot.return_value = stub_origin_snapshot mock_service_utils.lookup_snapshot_size.return_value = { 'revision': len(stub_origin_snapshot[0]), 'release': len(stub_origin_snapshot[1]) } origin_directory_url = reverse('browse-origin-directory', url_args={'origin_url': origin_info['url']}, query_params={'revision': revision_id_test}) origin_revision_log_url = reverse('browse-origin-log', url_args={'origin_url': origin_info['url']}, query_params={'revision': revision_id_test}) url = reverse('browse-revision', url_args={'sha1_git': revision_id_test}, query_params={'origin': origin_info['url']}) resp = self.client.get(url) self.assertContains(resp, origin_directory_url) self.assertContains(resp, origin_revision_log_url) for parent in revision_metadata_test['parents']: parent_url = reverse('browse-revision', url_args={'sha1_git': parent}, query_params={'origin': origin_info['url']}) self.assertContains(resp, '%s' % (parent_url, parent)) self.assertContains(resp, 'vault-cook-directory') self.assertContains(resp, 'vault-cook-revision') swh_rev_id = get_swh_persistent_id('revision', revision_id_test) swh_rev_id_url = reverse('browse-swh-id', url_args={'swh_id': swh_rev_id}) self.assertContains(resp, swh_rev_id) self.assertContains(resp, swh_rev_id_url) swh_dir_id = get_swh_persistent_id('directory', dir_id) swh_dir_id_url = reverse('browse-swh-id', url_args={'swh_id': swh_dir_id}) self.assertContains(resp, swh_dir_id) self.assertContains(resp, swh_dir_id_url) + self.assertContains(resp, 'swh-take-new-snapshot') + @patch('swh.web.browse.views.revision.service') def test_revision_log_browse(self, mock_service): per_page = 10 revision_history_log_test_sorted = \ sorted(revision_history_log_test, key=lambda rev: -parse_timestamp(rev['committer_date']).timestamp()) mock_revs_walker = MagicMock() mock_revs_walker.__iter__.return_value = revision_history_log_test_sorted mock_revs_walker.export_state.return_value = {} mock_service.get_revisions_walker.return_value = mock_revs_walker url = reverse('browse-revision-log', url_args={'sha1_git': revision_id_test}, query_params={'per_page': per_page}) resp = self.client.get(url) next_page_url = reverse('browse-revision-log', url_args={'sha1_git': revision_id_test}, query_params={'offset': per_page, 'per_page': per_page}) self.assertEqual(resp.status_code, 200) self.assertTemplateUsed('browse/revision-log.html') self.assertContains(resp, 'Newer') self.assertContains(resp, 'Older' % escape(next_page_url)) for log in revision_history_log_test_sorted[:per_page]: author_url = reverse('browse-person', url_args={'person_id': log['author']['id']}) revision_url = reverse('browse-revision', url_args={'sha1_git': log['id']}) self.assertContains(resp, log['id'][:7]) self.assertContains(resp, log['author']['name']) self.assertContains(resp, format_utc_iso_date(log['date'])) self.assertContains(resp, escape(log['message'])) self.assertContains(resp, format_utc_iso_date(log['committer_date'])) self.assertContains(resp, revision_url) resp = self.client.get(next_page_url) prev_page_url = reverse('browse-revision-log', url_args={'sha1_git': revision_id_test}, query_params={'per_page': per_page}) next_page_url = reverse('browse-revision-log', url_args={'sha1_git': revision_id_test}, query_params={'offset': 2 * per_page, 'per_page': per_page}) self.assertEqual(resp.status_code, 200) self.assertTemplateUsed('browse/revision-log.html') self.assertContains(resp, 'Newer' % escape(prev_page_url)) self.assertContains(resp, 'Older' % escape(next_page_url)) resp = self.client.get(next_page_url) prev_page_url = reverse('browse-revision-log', url_args={'sha1_git': revision_id_test}, query_params={'offset': per_page, 'per_page': per_page}) next_page_url = reverse('browse-revision-log', url_args={'sha1_git': revision_id_test}, query_params={'offset': 3 * per_page, 'per_page': per_page}) self.assertEqual(resp.status_code, 200) self.assertTemplateUsed('browse/revision-log.html') self.assertContains(resp, 'Newer' % escape(prev_page_url)) self.assertContains(resp, 'Older' % escape(next_page_url)) @patch('swh.web.browse.utils.service') @patch('swh.web.browse.views.revision.service') def test_revision_request_errors(self, mock_service, mock_utils_service): mock_service.lookup_revision.side_effect = \ NotFoundExc('Revision not found') url = reverse('browse-revision', url_args={'sha1_git': revision_id_test}) resp = self.client.get(url) self.assertEqual(resp.status_code, 404) self.assertTemplateUsed('error.html') self.assertContains(resp, 'Revision not found', status_code=404) mock_service.get_revisions_walker.side_effect = \ NotFoundExc('Revision not found') url = reverse('browse-revision-log', url_args={'sha1_git': revision_id_test}) resp = self.client.get(url) self.assertEqual(resp.status_code, 404) self.assertTemplateUsed('error.html') self.assertContains(resp, 'Revision not found', status_code=404) url = reverse('browse-revision', url_args={'sha1_git': revision_id_test}, query_params={'origin_type': 'git', 'origin': 'https://github.com/foo/bar'}) mock_service.lookup_revision.side_effect = None mock_utils_service.lookup_origin.side_effect = \ NotFoundExc('Origin not found') resp = self.client.get(url) self.assertEqual(resp.status_code, 404) self.assertTemplateUsed('error.html') self.assertContains(resp, 'the origin mentioned in your request appears broken', status_code=404)